/**
    Copyright (C) 2011  Alexander Vollmer

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.jefb.sec.service.impl;

import java.util.Date;

import javax.crypto.SecretKey;

import org.jefb.entity.DestinationEntity;
import org.jefb.entity.dto.FileToken;
import org.jefb.sec.entity.AgentAuthResponse;
import org.jefb.sec.entity.AgentAuthenticationRequest;
import org.jefb.sec.entity.AgentPermissions;
import org.jefb.sec.entity.ReceiverSession;
import org.jefb.sec.entity.SenderSession;
import org.jefb.sec.entity.TransInitRequest;
import org.jefb.sec.entity.TransInitResponse;
import org.jefb.sec.service.IAuthenticationService;
import org.jefb.sec.service.ITransInitService;
import org.jefb.service.IAppInitService;
import org.jefb.service.IRemoteProxy;
import org.jefb.service.ITransSessionStoreService;
import org.jefb.service.impl.ReceiverFactory;
import org.jefb.util.CryptographyUtil;
import org.jefb.util.JefbUtils;
import org.jefb.util.service.impl.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * This service contains methods for initialization of a session.
 * It performs agent authentication, prepare data (encryption) for transmission
 * and handle(check) data and permissions after receiving.
 * 
 * @author alexej
 * 
 */
@Service
public class TransInitService implements ITransInitService {
	//properties
	/**
	 * the properties below are hard coded
	 * at the moment only encrypted transmission with authentication is allowed
	 * after request for non-encrypted without authentication transmission
	 * we'll provide this functionality 
	 */
	private boolean encryptionAllowed = true;
	private boolean agentAuthenticationAllowed = true;
	
	private static Logger logger = LoggerFactory.getLogger(TransInitService.class);

	//services
	//util services
	@Autowired
	private Config config;
	
	//business services
	@Autowired
	private ITransSessionStoreService transSessionStoreService;
	@Autowired
	private IAuthenticationService authenticationService;
	@Autowired
	private IAppInitService appInitService;
	

	public TransInitResponse init(TransInitRequest transInitRequest) {
		TransInitResponse transInitResponse = new TransInitResponse();
		if (agentAuthenticationAllowed) {
			transInitResponse.setAuthenticationAllowed(true);
			// check agent id against database

		}
		if (encryptionAllowed) {
			transInitResponse.setPublicKey(appInitService.getPublicKey());
			transInitResponse.setEncryptionAllowed(true);
		}
		return transInitResponse;
	}

	public AgentAuthResponse auth(AgentAuthenticationRequest authRequest) {
		AgentAuthResponse agentAuthResponse = new AgentAuthResponse();

		// TODO decrypt username and password and perform authentication
		String username = JefbUtils.bytesToString(CryptographyUtil.decryptRSA(
				appInitService.getPrivateKey(), authRequest.getUsername()));
		String password = JefbUtils.bytesToString(CryptographyUtil.decryptRSA(
				appInitService.getPrivateKey(), authRequest.getPassword()));
		System.out.println("Authenticating user:" + username);
		if (authenticationService.authenticateAgent(username, password)) {
			// create new secure session for authenticated user
			ReceiverSession secureSession = new ReceiverSession();
			secureSession.setSenderPublicKey(authRequest.getSenderPublicKey());

			secureSession.setSenderAgentId(authRequest.getAgentId());
			// register session in session store
			String sessionId = transSessionStoreService
					.registerReceiverSession(secureSession);

			// set creation time
			secureSession.setCreationTime(new Date());
			secureSession.setLastAccessTime(new Date());

			// decrypt sender AES key and store it in session
			SecretKey aesKey = CryptographyUtil.decryptAESKey(authRequest
					.getKey(), appInitService.getPrivateKey());

			secureSession.setSenderAESKey(aesKey);

			agentAuthResponse.setAuthSuccied(true);

			byte[] encryptedSessionId = CryptographyUtil.encryptRSA(authRequest
					.getSenderPublicKey(), sessionId.getBytes());

			agentAuthResponse.setSessionId(encryptedSessionId);
		} else {
			agentAuthResponse.getErrorMessages().add(
					"Authentication failure. Bad credentials.");
			agentAuthResponse.setAuthSuccied(false);
		}
		return agentAuthResponse;
	}

	public boolean isEncryptionAllowed() {
		return encryptionAllowed;
	}

	public void setEncryptionAllowed(boolean encryptionAllowed) {
		this.encryptionAllowed = encryptionAllowed;
	}

	public boolean isAuthenticationAllowed() {
		return agentAuthenticationAllowed;
	}

	public void setAuthenticationAllowed(boolean authenticationAllowed) {
		this.agentAuthenticationAllowed = authenticationAllowed;
	}

	public AgentPermissions checkAgent(String agentId, byte[] sessionId) {
		ReceiverSession receiverSession = transSessionStoreService
				.getReceiverSession(agentId);
		AgentPermissions agentPermissions = new AgentPermissions();
		if (receiverSession == null) {
			agentPermissions.setSessionRegistered(false);
			return agentPermissions;
		}
		// TODO init agent rights, cache them in session
		byte[] decryptedSessionId = CryptographyUtil.decryptRSA(appInitService
				.getPrivateKey(), sessionId);
		String sessionIdPlain = JefbUtils.bytesToString(decryptedSessionId);
		if (!sessionIdPlain.equals(receiverSession.getSessionId())) {
			agentPermissions.setSessionRegistered(false);
			return agentPermissions;
		}

		agentPermissions.setSessionRegistered(true);
		return agentPermissions;
	}

	public void setAuthenticationService(
			IAuthenticationService authenticationService) {
		this.authenticationService = authenticationService;
	}

	public IAuthenticationService getAuthenticationService() {
		return authenticationService;
	}

	public void setAppInitService(IAppInitService appInitService) {
		this.appInitService = appInitService;
	}

	public IAppInitService getAppInitService() {
		return appInitService;
	}

	public void setTransSessionStoreService(
			ITransSessionStoreService transSessionStoreService) {
		this.transSessionStoreService = transSessionStoreService;
	}

	public ITransSessionStoreService getTransSessionStoreService() {
		return transSessionStoreService;
	}

	public FileToken checkToken(FileToken fileToken) {
		if (encryptionAllowed) {
			ReceiverSession receiverSession = transSessionStoreService
					.getReceiverSession(fileToken.getSenderAgentId());
			if (receiverSession != null) {
				SecretKey senderAESKey = receiverSession.getSenderAESKey();
				byte[] decryptedData = CryptographyUtil.decryptAES(
						senderAESKey, fileToken.getData());
				fileToken.setData(decryptedData);
			} else {
				// TODO handle if session doesn't exists
			}
		}
		return fileToken;
	}

	public IRemoteProxy getReceiver(DestinationEntity destination) {
		//obtain sender session
		SenderSession senderSession = transSessionStoreService.getSenderSession(destination);
		//if sender session doesn't exist, create new one
		if (senderSession == null) {
			// create receiver proxy
			IRemoteProxy remoteProxy = ReceiverFactory
					.createReceiverProxy(destination.getAddress());
			// init and authenticate receiver proxy
			TransInitRequest transInitRequest = new TransInitRequest();
			TransInitResponse init = null;
			try {
				init = remoteProxy.init(transInitRequest);
			} catch (Exception e) {
				// TODO log in the database //dest X, exception message
				e.printStackTrace();
				logger.error("Unable to initialize receiver.", e);
			}

			if (init != null) {
				// perform authentication
				if (!init.isAuthenticationAllowed()) {
					// TODO implement it
				} else {
					AgentAuthenticationRequest authRequest = new AgentAuthenticationRequest();

					authRequest.setUsername(CryptographyUtil.encryptRSA(init
							.getPublicKey(), destination.getUsername()
							.getBytes()));
					authRequest.setPassword(CryptographyUtil.encryptRSA(init
							.getPublicKey(), destination.getPassword()
							.getBytes()));

					SecretKey aesKey = CryptographyUtil.generateAESKey();
					System.out.println("AES KEY:"
							+ JefbUtils.toHex(aesKey.getEncoded()));
					authRequest.setKey(CryptographyUtil.encryptRSA(init
							.getPublicKey(), aesKey.getEncoded()));
					
					authRequest.setSenderPublicKey(appInitService.getPublicKey());
					authRequest.setAgentId(config.getAgentId());
					
					AgentAuthResponse auth = remoteProxy.auth(authRequest);
					if (auth.isAuthSuccied()) {
						SenderSession newSenderSession = new SenderSession();
						newSenderSession.setCreationTime(new Date());
						newSenderSession.setLastAccessTime(new Date());
						newSenderSession.setDestId(destination.getAddress());
						newSenderSession.setReceiverPublicKey(init
								.getPublicKey());
						newSenderSession.setSenderAESKey(aesKey);
						
						//TODO handle if encryption is desabled
						// decrypt session id
						byte[] sessionId = CryptographyUtil.decryptRSA(appInitService.getPrivateKey(),
								auth.getSessionId());				
						newSenderSession.setSessionId(JefbUtils
								.bytesToString(sessionId));
						
						//encrypt session id again to use it for transmission
						byte[] encryptedSessionId = CryptographyUtil.encryptRSA(init.getPublicKey(), sessionId);
						newSenderSession.setEncryptedSessionId(encryptedSessionId);
						
						newSenderSession.setEncryprionAllowed(init.isEncryptionAllowed());
						newSenderSession.setRemoteProxy(remoteProxy);
						transSessionStoreService.registerSenderSession(newSenderSession);
						return newSenderSession.getRemoteProxy();
					} else {
						logger.warn("Authentication failure for destination:"
								+ destination.getAddress());
					}
				}
			}
		}
		return senderSession.getRemoteProxy();
	}

	public FileToken prepareToken(FileToken fileToken, DestinationEntity destination) {
		SenderSession senderSession = transSessionStoreService.getSenderSession(destination);
		if(senderSession.isEncryprionAllowed()){
			fileToken.setSessionId(senderSession.getEncryptedSessionId());
			fileToken.setData(CryptographyUtil.encryptAES(senderSession.getSenderAESKey(), fileToken.getData()));
		}else{
			fileToken.setSessionId(senderSession.getSessionId().getBytes());
		}
		return fileToken;
	}

	public void setConfig(Config config) {
		this.config = config;
	}

	public Config getConfig() {
		return config;
	}

}
